﻿
/*******************************************************************************************
// ©2009 Microsoft Corporation.  This code is provided under the Microsoft Public License.
*******************************************************************************************/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using Microsoft.Rtc.Collaboration;
using Microsoft.Rtc.Collaboration.Presence;
using System.Net;
using System.Net.Mail;
using System.Xml;
using System.Xml.Linq;
using System.IO;
using System.Configuration;
using System.Collections.Specialized;
using System.Security.Principal;
using Microsoft.Rtc.Signaling;
using System.ServiceModel.Description;
using System.ServiceModel.Activation;
using System.Threading;
using System.Globalization;
using System.Web;
using UCMACellularWS.GeocodeService;


namespace UCMACellularWS
{
    /// <summary>
    /// This class implements methods for publish/subscribe sample
    /// </summary>
    public class PubSub : IPubSub
    {
        
        #region Private members
        private static PubSubPlatformConfiguration _config= new PubSubPlatformConfiguration();
        private static PubSubLogger _logger;

        //UCMA member variables
        private static CollaborationPlatform _collabPlatform;
        private static UserEndpoint _botEndPoint;
              
        private GPSLocation location;
        private static TargetUserConfiguration _imUser;
        
        //Publish Presence schema. This is a schema to publish state category which is built in presence category
        private const string OCS_STATE_SCHEMA = "<state xsi:type=\"phoneState\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://schemas.microsoft.com/2006/09/sip/state\"><availability>{0}</availability><activity><custom LCID=\"1033\">{1}</custom></activity></state>";

        //Custom Presence Schema. This has to be in valid XML format. The name of the custom category must be
        //registered on OCS server.
        private const string OCS_CUSTOM_PRESENCE= "<publishRawCategories rid=\"1\"><rawCategory name=\"gpsLocation\" instanceMask=\"1\" version=\"1\" container=\"{0}\" endpointId=\"999\" expireType=\"static\" ><gpsLocation>{1}</gpsLocation></rawCategory></publishRawCategories>";

        //This is the ID of the "company" presence access level container.
        private const int OCS_COMPANY_CONTAINER_ID = 200;
        
        #endregion

        #region Enumerations
        /// <summary>
        /// Presence Category Enumeration
        /// </summary>
        private enum Categories
        {
            state,
            gpsLocation
        };
        #endregion

        #region Internal Properties
        /// <summary>
        /// Platform configuration settings
        /// </summary>
        internal static PubSubPlatformConfiguration Config
        {
            get
            {
                return _config;
            }
            set
            {
                _config = value;
            }
        }

        /// <summary>
        /// Logs exceptions and other events in Event viewer.
        /// </summary>
        internal static PubSubLogger Logger
        {
            get
            {
                return _logger;
            }
            set
            {
                _logger = value;
            }
        }

        /// <summary>
        /// Collaboration platform
        /// </summary>
        internal static CollaborationPlatform CollabPlatform
        {
            get
            {
                return _collabPlatform;
            }
            set
            {
                _collabPlatform = value;
            }
        }

        /// <summary>
        /// Bot End point for the service
        /// </summary>
        internal static UserEndpoint BotEndPoint
        {
            get
            {
                return _botEndPoint;
            }
            set
            {
                _botEndPoint = value;
            }
        }

        #endregion
        
        #region Constructor
        public PubSub()
        {}
        #endregion
        
        #region IPubSub Members

        /// <summary>
        /// Allows Cell phone infrastructure to update user presence and activity. It can be generic like On Hook or Off Hook or they can provide custom message for the activity
        /// </summary>
        /// <param name="status">Presence status of the cell phone user</param>
        /// <param name="sipAddress">sip address of the target user</param>
        /// <param name="customMessage">Custom activity message</param>
        /// <returns>returns true if operation was successful otherwise false</returns>
        public bool UpdatePhoneStatus(CellPhoneStatus status, string sipAddress,string customMessage)
        {
            string stateBody = "";
            if(string.IsNullOrEmpty(sipAddress))
                throw new InvalidDataException("No SIP address provided.");

            //validate to see whether this user has been registered in config file or not
            bool validated = ValidateUser(sipAddress);
            if (!validated)
            {
                throw new InvalidDataException("Invalid SIP address.");
            }
                    
            //Construct XML Schema for state
            if (string.IsNullOrEmpty(customMessage.Trim()))
                stateBody = ConstructSchema((int)status, status.ToString());
            else
                stateBody = ConstructSchema((int)status, customMessage);
            try
            {
                PublishPresence(Categories.state.ToString(),stateBody,sipAddress);
            }
            catch (Exception ex)
            {
                return false;
            }
            
            //return true if succes otherwise false
            return true;
        }

        /// <summary>
        /// Allows clients to publish GPS Location as a Custom Presence
        /// </summary>
        /// <param name="sipAddress">Sip Address of the target user</param>
        /// <param name="gpsStatement">GPS statement in NMEA approved format</param>
        /// <returns>returns true if operation was successful otherwise false</returns>
        public bool PublishGPSLocation(string sipAddress, string gpsStatement)
        {
            string stateBody = "";

            //sip address required.
            if (string.IsNullOrEmpty(sipAddress))
                throw new InvalidDataException("No SIP address provided.");

            //validate to see whether this user has been registered in config file or not
            bool validated = ValidateUser(sipAddress);
            if (!validated)
            {
                throw new InvalidDataException("Invalid SIP address.");
            }

            if (string.IsNullOrEmpty(gpsStatement) || gpsStatement.Trim().Length <= 0)
                throw new InvalidDataException("No GPS statement provided.");
            else
            {
                location = new GPSLocation();

                //parse GPS location
                ParseGPSStatement(gpsStatement);
                stateBody = ConstructCustomSchema(MakeReverseGeocodeRequest());
            }
            try
            {
                //Setup collaboration platform and endpoint
                PublishPresence(Categories.gpsLocation.ToString(),stateBody,sipAddress);
            }
            catch (Exception ex)
            {
                return false;
            }
            //return true if succes otherwise false
            return true;
        }
        #endregion
        
        #region Private methods
        /// <summary>
        /// This method builds the proper XML schema
        /// </summary>
        /// <param name="avail">Availibility value</param>
        /// <param name="activity">Activity string</param>
        /// <returns>XML schema string</returns>
        private string ConstructSchema(int availability, string activity)
        {
            return string.Format(OCS_STATE_SCHEMA, availability, activity);
        }

        /// <summary>
        /// Build schema for Custom Presence
        /// </summary>
        /// <param name="location"></param>
        /// <returns></returns>
        private string ConstructCustomSchema(string location)
        {
            return string.Format(OCS_CUSTOM_PRESENCE, OCS_COMPANY_CONTAINER_ID, location);
        }

        /// <summary>
        /// Retrieve the location using Ms Live Maps API. It uses the latitude and longitude parsed out from GPS statement.
        /// </summary>
        /// <returns></returns>
        private string MakeReverseGeocodeRequest()
        {
            try
            {
                // Get a Virtual Earth token before making a request
                string token = GetToken();
                ReverseGeocodeRequest reverseGeocodeRequest = new ReverseGeocodeRequest();

                // Set the credentials using a valid Virtual Earth token
                reverseGeocodeRequest.Credentials = new Credentials();
                reverseGeocodeRequest.Credentials.Token = token;

                // Set the point to use to find a matching address
                GeocodeService.Location point = new GeocodeService.Location();
                point.Latitude = double.Parse(location.Latitude);
                point.Longitude = double.Parse(location.Longitude);

                reverseGeocodeRequest.Location = point;

                // Make the reverse geocode request
                GeocodeService.GeocodeService geocodeService = new GeocodeService.GeocodeService();
                GeocodeResponse response = geocodeService.ReverseGeocode(reverseGeocodeRequest);
                if (response.Results.Length > 0)
                    return response.Results[0].DisplayName;
                else
                    return "No location found";
            }
            catch (Exception ex)
            {
                _logger.Log(ex);
                throw ex;
            }
        }
        
        /// <summary>
        /// Validate Ms live developer sccount id with the server and gets the verification token back which will be used to make reverse geocode call.
        /// </summary>
        /// <returns></returns>
        private string GetToken()
        {
            // Set Virtual Earth Platform Developer Account credentials to access the Token Service
            TokenService.CommonService commonService = new TokenService.CommonService();
            commonService.Credentials = new System.Net.NetworkCredential(_config.liveAccountID, _config.liveAccountPwd);

            // Set the token specification properties
            TokenService.TokenSpecification tokenSpec = new TokenService.TokenSpecification();
            tokenSpec.ClientIPAddress = _config.clientIP;
            tokenSpec.TokenValidityDurationMinutes = _config.tokenDuration;

            string token = "";
            // Get a token
            try
            {
                token = commonService.GetClientToken(tokenSpec);
                if (string.IsNullOrEmpty(token.Trim()))
                    throw new AuthenticationException("Invalid Account ID/Password.");
            }
            catch (AuthenticationException ex)
            {
                throw ex;
            }
            catch (Exception ex)
            {
                throw ex;
            }

            return token;
        }

        /// <summary>
        /// Parse GPS Statement to get Latitude and Longitude 
        /// </summary>
        /// <param name="statement">GPS Statement in NMEA approved format</param>
        private void ParseGPSStatement(string statement)
        {
            string[] statementArr = statement.Split('$');
            for (int i = 0; i < statementArr.Length; i++)
            {
                string tempStatement = statementArr[i];
                string[] lineArr = tempStatement.Split(',');
                if (lineArr[0] == "GPGGA")
                {
                    try
                    {
                        //Parse Latitude
                        Double latitude = Convert.ToDouble(lineArr[2]);
                        latitude = latitude / 100.0;
                        string[] latDegree = latitude.ToString().Split('.');
                        string latsign = "";
                        if (lineArr[3].ToString().Equals("S"))
                            latsign="-";
                        location.Latitude = latsign + latDegree[0].ToString() + "." +
                                    ((Convert.ToDouble(latDegree[1]) / 60)).ToString("#####");
                                                
                        //Parse Longitude
                        Double longitude = Convert.ToDouble(lineArr[4]);
                        longitude = longitude / 100;
                        string[] lonDegree = longitude.ToString().Split('.');
                        string lonsign = "";
                        if (lineArr[5].ToString().Equals("W"))
                            lonsign = "-";
                        location.Longitude = lonsign + lonDegree[0].ToString() + "." +
                                        ((Convert.ToDouble(lonDegree[1]) / 60)).ToString("#####");
                    }
                    catch (Exception ex)
                    {
                        _logger.Log(ex);

                        //throw invalid data exception back to the client
                        throw new InvalidDataException("Invalid GPS Statement.");
                    }
                }
            }
        }
        
        /// <summary>
        /// Compares provided sip address with the list form the config file and validate if this user is registered or not
        /// </summary>
        /// <param name="sipAddress">sip address of the user</param>
        /// <returns>True if user is valid otherwise false</returns>
        private bool ValidateUser(string sipAddress)
        {       
            //go through the list of target users and compare the provided sip address with uri
            List<TargetUserConfiguration> TargetUsers = _config.TargetUsers;

            bool exists = false;
            foreach (TargetUserConfiguration user in TargetUsers)
            {
                if (user.Uri.ToLower().Equals(sipAddress.ToLower()))
                {
                    //user exists
                    exists = true;
                    break;
                }
            }
            return exists;
        }
        
        /// <summary>
        /// This is handler will catch the notification sent if target users' presence changes
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void HandlerPresenceNotificationReceived(object sender, RemotePresenceNotificationEventArgs e)
        {
            //Multiple presence notifications may be received in one event
            foreach (RemotePresentityNotificationData notification in e.Notifications)
            {
                //Check to see if this contact is online using our method previous UCMA lab
                bool isAway = IsUserAway(notification);

                //go thorugh all the targetusers in the config and find the one and set the away boolean for that user
                foreach (TargetUserConfiguration user in _config.TargetUsers)
                {
                    if(user.Uri.ToLower().Equals(notification.Uri.ToLower()))
                    {
                        user.IsAway = isAway;
                    }
                }
            }
        }

        /// <summary>
        /// Method to find if particular user is Away or Offline
        /// </summary>
        /// <param name="notification"></param>
        /// <returns>returns true if user is away or offline otherwise false.</returns>
        private static bool IsUserAway(RemotePresentityNotificationData notification)
        {
            bool isAway = false;
            // first, are they only providing old style presence?
            foreach (PresenceCategoryWithMetaData category in notification.Categories)
            {
                if (category.Name.Equals("state"))
                {
                    // Get the XML data.
                    string rawXml = category.CreateInnerDataXml();
                    if (rawXml == null || rawXml.Trim().Length == 0)
                    {
                        break;
                    }                                          
                    
                    //parse availability using linq to xml
                    XDocument configDoc;
                    configDoc = XDocument.Parse(rawXml);
                    XNamespace nspace = XNamespace.Get("http://schemas.microsoft.com/2006/09/sip/state");
                    var availability = from state in configDoc.Descendants(nspace + "state")
                               select state.Element(nspace + "availability").Value;

                    //if availability is away or offline mark user as away
                    isAway = ((Convert.ToInt64(availability.ElementAt(0)) >= (long)PresenceAvailability.Away) && (Convert.ToInt64(availability.ElementAt(0)) <= (long)PresenceAvailability.Offline));
                }
            }
            return isAway;
        }
        
        /// <summary>
        /// This method subscribe to the presence of all the users
        /// </summary>
        internal static void SubscribeToPresence()
        {
            //Create Endpoints for each user.
            foreach (TargetUserConfiguration user in _config.TargetUsers)
            {
                UserEndpoint endPoint;
                UserEndpointSettings settings = new UserEndpointSettings(user.Uri, _config.OcsServerFqdn, _config.OcsServerTlsPort);
                endPoint = new UserEndpoint(_collabPlatform, settings);
                endPoint.EndEstablish(endPoint.BeginEstablish(null, null));
                
                //assign endpoint to the TargetUserObject
                user.EndPoint = endPoint;

                // Hook event for presence notifications prior to adding the targets
                endPoint.RemotePresence.PresenceNotificationReceived +=
                  new EventHandler<RemotePresenceNotificationEventArgs>
                    (HandlerPresenceNotificationReceived);

                // Prepare list of contacts for presence subscription
                List<RemotePresentitySubscriptionTarget> subscriptionTargets =
                new List<RemotePresentitySubscriptionTarget>();
                
                //Add the same user
                subscriptionTargets.Add(new RemotePresentitySubscriptionTarget(user.Uri, null));

                //Add the list of contacts to subscribe
                //Immediately returns first event w/current presence
                endPoint.RemotePresence.EndAddTargets(endPoint.RemotePresence.BeginAddTargets(subscriptionTargets,null,null));

                //Register for incoming IMCall
                endPoint.RegisterForIncomingCall<InstantMessagingCall>(InstantMessagingCallReceived);
            }
        }

        /// <summary>
        /// This handler will execute in the event of IM call being received for perticular userendpoint
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void InstantMessagingCallReceived(object sender, CallReceivedEventArgs<InstantMessagingCall> e)
        {
            UserEndpoint endPoint=sender as UserEndpoint;
            
            //Get the Message that was sent to the user
            string message = e.ToastMessage.Message;
            foreach (TargetUserConfiguration user in _config.TargetUsers)
            {
                if(user.Equals(endPoint.OwnerUri))
                {
                    //If endpoint uri matched one of the target user then mark the user and break the loop
                    _imUser=user;
                    break; 
                }
            }

            //Send an SMS to a user only if user is away
            if (_imUser.IsAway)
            {
                try
                {
                    //Accept call
                    e.Call.EndAccept(e.Call.BeginAccept(null, null));

                    InstantMessagingFlow massageFlow = e.Call.Flow;
                    massageFlow.EndSendMessage(massageFlow.BeginSendMessage(_config.GreetMessage, null, null));
                    massageFlow.MessageReceived += new EventHandler<InstantMessageReceivedEventArgs>(MessageReceived);
                }
                catch (RealTimeException exception)
                {
                    // RealTimeException may be thrown on media or link-layer failures.
                    _logger.Log(exception);
                }
            }
        }

        /// <summary>
        /// This handler will execute in event of an IM message received
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void MessageReceived(object sender,InstantMessageReceivedEventArgs e)
        {
            if (_imUser.IsAway)
            {
                InstantMessagingFlow messageFlow = (InstantMessagingFlow)sender;
                string message = e.TextBody;

                //Send Message through SMS
                SendSMS(message, e.Sender.Uri.Remove(0, 4));

                //reply back to user
                messageFlow.EndSendMessage(messageFlow.BeginSendMessage(_config.Reply, null, null));
            }
        }
        
        /// <summary>
        /// This method sends out SMS message (via email for now)
        /// </summary>
        /// <param name="message">Message to be sent</param>
        /// <param name="sender">Sender of the message</param>
        private static void SendSMS(string message,string sender)
        {
            try
            {
                MailMessage emailMessage = new MailMessage(sender, _imUser.SMSAddress, _config.EmailSubject, message); 
                SmtpClient mailClient = new SmtpClient(_config.SmtpServer, _config.SmtpPort);
                mailClient.Credentials = (ICredentialsByHost)CredentialCache.DefaultNetworkCredentials;
                mailClient.Send(emailMessage);
            }
            catch (FormatException e)
            {
                //Invalid recepient email address
                _logger.Log("Invalid recepient email address.",e);
            }
            catch (SmtpException smtpEx)
            {
                //Exception was thrwn when trying to connect to smtp server
                _logger.Log("Can not connect to smtp server.",smtpEx);
            }
            catch (InvalidOperationException invalidEX)
            {
                _logger.Log(invalidEX);
            }
        }

        /// <summary>
        /// Perform cleanup when service will shutdown
        /// </summary>
        internal static void Cleanup()
        {
            //close every user end points
            foreach (TargetUserConfiguration user in _config.TargetUsers)
            {
                user.EndPoint.RemotePresence.EndRemoveAllTargets(user.EndPoint.RemotePresence.BeginRemoveAllTargets(null,null));
                user.EndPoint.EndTerminate(user.EndPoint.BeginTerminate(null, null));
            }

            //Shutdown the CollaborationPlatform
            _collabPlatform.EndShutdown(_collabPlatform.BeginShutdown(null, null));
        }
        
        /// <summary>
        /// Publishes the presence of the target user
        /// </summary>
        /// <param name="machineStateBody"></param>
        private void PublishPresence(string category,string stateBody, string sipAddress)
        {
            try
            {
                UserEndpointSettings uSettings = new UserEndpointSettings(sipAddress,
                    _config.OcsServerFqdn,
                    _config.OcsServerTlsPort);

                UserEndpoint endPoint = new UserEndpoint(_collabPlatform, uSettings);
                endPoint.EndEstablish(endPoint.BeginEstablish(null, null));

                //Get a handle to the LocalOwnerPresence of the endpoint
                LocalOwnerPresence userPresence;
                userPresence = endPoint.LocalOwnerPresence;

                //Create the CategoryMetadata for the state category
                //passing in the state that we need to publisn
                PresenceCategoryWithMetaData state =
                  new PresenceCategoryWithMetaData(
                  1, //Instance ID. This is just an arbitrary value
                  OCS_COMPANY_CONTAINER_ID,
                  new CustomPresenceCategory(category, stateBody));
               
                //Create a list of the categories to publish
                List<PresenceCategoryWithMetaData> itemList = new List<PresenceCategoryWithMetaData>();
                itemList.Add(state);

                //Publish presence
                userPresence.EndPublishPresence(userPresence.BeginPublishPresence(itemList, null, null));
            }
            catch (ConnectionFailureException connFailEx)
            {
                // ConnectionFailureException will be thrown when the endpoint cannot connect to the server, or the credentials are invalid.
                _logger.Log(connFailEx);
                throw connFailEx;
            }
            catch (InvalidOperationException iOpEx)
            {
                // InvalidOperationException will be thrown when the endpoint is not in a valid state to connect. To connect, the platform must be started and the Endpoint Idle.
                _logger.Log(iOpEx);
                throw iOpEx;
            }
            catch (RegisterException regEx)
            {
                // RegisterException will be thrown when the endpoint cannot be registered (usually due to bad credentials).
                _logger.Log(regEx);
                throw regEx;
            }
        }
        #endregion
      
    }

    /// <summary>
    /// GPS Latitude and Longitude to be published
    /// </summary>
    public class GPSLocation
    {
        public string Latitude { get; set; }
        public string Longitude { get; set; }
    }
}
